﻿/*
Discio
Copyright (c) 2013 - Walter M. Soto Reyes
Microsoft Public License (Ms-PL)
License Agreement:  https://discio.codeplex.com/license
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Security.Cryptography.Xml;

namespace Discio
{
    /// <summary>
    /// Manage a Discio repository
    /// </summary>
    public class DiscioManager
    {
        /// <summary>
        /// Default Constructor
        /// </summary>
        public DiscioManager() 
        {
            Location = "";
            ToDelete = new List<IDiscio>();
            ToInsert = new List<IDiscio>();
            ToUpdate = new List<IDiscio>();
        }

        /// <summary>
        /// Constructor w/ Setname
        /// </summary>
        /// <param name="setName">Name of the Xml dataset</param>
        public DiscioManager(string setName)
        {
            Location = "";
            SetName = setName;
            Start();
            ToDelete = new List<IDiscio>();
            ToInsert = new List<IDiscio>();
            ToUpdate = new List<IDiscio>();
        }

        /// <summary>
        /// Constructor with setname and location
        /// </summary>
        /// <param name="setName">Name of the Xml  dataset</param>
        /// /// <param name="location">Location refers to a subfolder within the DataSource folder</param>
        public DiscioManager(string setName,string location)
        {
            SetName = setName;
            Location = location;
            Start();
            ToDelete = new List<IDiscio>();
            ToInsert = new List<IDiscio>();
            ToUpdate = new List<IDiscio>();
        }

        /// <summary>
        /// Check if a setname exists
        /// </summary>
        /// <param name="name">SetName</param>
        /// <returns>True/False</returns>
        public static bool IsDataSetValid(string name)
        {
            return IsDataSetValid(name, "");
        }

        /// <summary>
        /// Check if a data set exists
        /// </summary>
        /// <param name="name">Data set name</param>
        /// <param name="location">Location</param>
        /// <returns>True/False</returns>
        public static bool IsDataSetValid(string name, string location)
        {
            string datafolder = "";
            if (Directory.Exists(DiscioSource.DataFolder))
            {
                datafolder = DiscioSource.DataFolder.TrimEnd(@"\".ToCharArray()).TrimEnd('/');
            }
            else
            {
                return false; 
            }

            string fullPath = datafolder + @"\" + name + "." + DiscioSource.Extension;

            if (location.Length > 0)
            {
                if (Directory.Exists(datafolder + @"\" + location))
                {
                    fullPath = datafolder + @"\" + location + @"\" + name + "." + DiscioSource.Extension;
                }
                else
                {
                    return false;
                }
            }
             
            return File.Exists(fullPath);
        }

          /// <summary>
        /// Create a dataset if missing
        /// </summary>
        /// <param name="name">Dataset name</param>
        /// <returns>True/False</returns>
        public static bool CreateSetIfMissing(string name) 
        {
            return CreateSetIfMissing(name, "");
        }

        /// <summary>
        /// Create a dataset if missing
        /// </summary>
        /// <param name="name">Dataset name</param>
        /// <param name="location">Location</param>
        /// <returns>True/False</returns>
        public static bool CreateSetIfMissing(string name, string location)
        {
            if (!IsDataSetValid(name, location)) {
              return  CreateSet(name, location);
            }

            return false;
        }

        /// <summary>
        /// Create a data set
        /// </summary>
        /// <param name="name">Set name</param> 
        /// <returns>True/False</returns>
        public static bool CreateSet(string name)
        {
           return CreateSet(name, "");
        }

        /// <summary>
        /// Create a data set
        /// </summary>
        /// <param name="name">Set name</param>
        /// <param name="location">Location</param>
        /// <returns>True/False</returns>
        public static bool CreateSet(string name, string location)
        {
            string datafolder = "";
            if (Directory.Exists(DiscioSource.DataFolder))
            {
                datafolder = DiscioSource.DataFolder.TrimEnd(@"\".ToCharArray()).TrimEnd('/');

                string fullPath = datafolder + @"\" + name + "." + DiscioSource.Extension;

                if (location.Length > 0)
                {
                    if (Directory.Exists(datafolder + @"\" + location))
                    {
                        fullPath = datafolder + @"\" + location + @"\" + name + "." + DiscioSource.Extension;  
                    } 
                }

                if (!File.Exists(fullPath))
                {
                    string content = @"<discio_" + name + "></discio_" + name + ">";
                    File.WriteAllText(fullPath, content, ASCIIEncoding.UTF8);
                    return true;
                }


            }

            return false;
        }

        /// <summary>
        /// Start a repository
        /// </summary>
        /// <remarks>
        /// Use this method if you created an instance of the manager using the default constructor.
        /// </remarks>
        public void Start()
        {
            if (Directory.Exists(DiscioSource.DataFolder))
            {
                DataFolder = DiscioSource.DataFolder.TrimEnd(@"\".ToCharArray()).TrimEnd('/');
            }
            else
            {
                ValidSource = false;
                throw new DiscioException("Invalid DiscioSource.DataFolder");
            }

            if (this.SetName.Length > 0)
            {
                ValidSource = true; 

                //Create set if needed
                FullPath = DataFolder + @"\" + SetName + "."+DiscioSource.Extension;
           
                if (Location.Length > 0)
                {
                    if(Directory.Exists(DataFolder + @"\" + Location)){
                        FullPath = DataFolder + @"\" + Location + @"\" + SetName + "." + DiscioSource.Extension;
                    }
                    else
                    {
                        throw new DiscioException("Location does not exists within the datafolder");
                    }
                }

                if (DiscioSource.AutoCreateSets)
                {
                    DiscioManager.CreateSetIfMissing(SetName, Location); 
                }
                else
                {
                    //Use DiscioManager.CreateSet(string name);
                    if (!File.Exists(FullPath))
                    {
                        throw new DiscioException("SetName does not exists - User DiscioManager.CreateSet(string name) first.");
                    }
                }
                
                if (File.ReadAllText(FullPath).Length < 2)
                {
                    string content = @"<discio_" + SetName + "></discio_" + SetName + ">";
                    File.WriteAllText(FullPath, content, ASCIIEncoding.UTF8); 
                }
                
            }
            else
            {
                ValidSource = false;
                throw new DiscioException("Invalid SetName");
            }
        }

        /// <summary>
        /// Commit an operation
        /// </summary>
        /// <remarks>
        /// Insert/Update/Delete
        /// </remarks>
        public void Commit()
        {

            InsertItems();
            UpdateItems();
            DeleteItems();

        }

       
        private void InsertItems()
        {
            if (ToInsert.Count > 0)
            {
                CreateGUIDs();
                XmlDocument doc = new XmlDocument();
                doc.Load(FullPath);

                foreach (IDiscio i in this.ToInsert)
                {
                    XmlElement elem = i.SerializeToXmlElement();
                    XmlNode node = doc.CreateElement(i.GetType().Name);
                    node.InnerXml = elem.InnerXml;
                    XmlNode f = doc.DocumentElement.FirstChild;
                    if (f != null)
                    {
                        doc.DocumentElement.InsertBefore(node, f);
                    }
                    else
                    {
                        doc.DocumentElement.AppendChild(node);
                    }
                    elem = null;
                    node = null;
                    f = null;

                }

                doc.Save(FullPath);
                this.ToInsert.Clear();
                doc = null;
            }
        }

        private void UpdateItems()
        {
            if (ToUpdate.Count > 0)
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(FullPath);

                foreach (IDiscio i in ToUpdate)
                {

                    XmlNode node = doc.DocumentElement;
                    string xpath = @"/discio_" + SetName + @"/" + i.GetType().Name + @"[ID='" + i.ID + "']";
                    node = doc.SelectSingleNode(xpath);
                    if (node != null)
                    {
                        node.InnerXml = i.SerializeToXmlElement().InnerXml;

                    }
                }
                doc.Save(FullPath);
                ToUpdate.Clear();
                doc = null;
            }
        }

        private void DeleteItems()
        {
            if (ToDelete.Count > 0)
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(FullPath);

                foreach (IDiscio i in ToDelete)
                {
                    XmlNode node = doc.DocumentElement;
                    string xpath = @"/discio_" + SetName + @"/" + i.GetType().Name + @"[ID='" + i.ID + "']";
                    node = doc.SelectSingleNode(xpath);
                    if (node != null)
                    {
                        doc.DocumentElement.RemoveChild(node);
                    }
                }
                doc.Save(FullPath);
                ToDelete.Clear();
                doc = null;
            }
        }

        #region "Linq Queries"

        /// <summary>
        /// Remove all items in a set
        /// </summary>
        /// <returns>True/False</returns>
        public bool RemoveAll<T>()
        {  
            XmlDocument doc = new XmlDocument();
            doc.Load(FullPath);
            string name = typeof(T).Name;
            var nodes = doc.SelectNodes("//" + name);
            foreach (XmlNode n in nodes)
            {
                doc.DocumentElement.RemoveChild(n);
            }
            doc.Save(FullPath); 
            doc = null; 

            return true;
        }

        /// <summary>
        /// Get list of items by page with where clause
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <param name="page">Page</param>
        /// <param name="size">Page size</param>
        /// <param name="where">Where clause</param>
        /// <returns>Page as List</returns>
        public List<T> Page<T>(int page, int size, Func<T, bool> where)
        {
            XElement elem = XElement.Load(FullPath);
            return (from m in elem.Elements(typeof(T).Name)
                    select m.ToXmlElement().DeserializeFromXmlElement<T>()).Where(where)
                         .Skip((page - 1) * size).Take(size).ToList();
        }

        /// <summary>
        /// Get list of items by page
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <param name="page">Page</param>
        /// <param name="size">Page Size</param>
        /// <returns>Page as List</returns>
        public List<T> Page<T>(int page, int size=5)
        {
            XElement elem = XElement.Load(FullPath);
            return (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>())
                         .Skip((page - 1)*size).Take(size).ToList();
        }

        /// <summary>
        /// Get number of pages in dataset with where clause
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <param name="size">Page Size</param>
        /// <returns>Page Count</returns>
        public int PageCount<T>(double size, Func<T, bool> where)
        {
            double count = (double)RowCount<T>(where);  
            return (int)Math.Ceiling((double)(count / size));
        }

        /// <summary>
        /// Get number of pages in dataset
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <param name="size">Page Size</param>
        /// <returns>Page Count</returns>
        public int PageCount<T>(double size)
        {
            double count = (double)RowCount<T>();
            return (int)Math.Ceiling((double)(count / size));
        }

        /// <summary>
        /// Get RowCount of items in dataset with where clause
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <returns>Row total</returns>
        public int RowCount<T>(Func<T, bool> where)
        { 
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>()).Where(where);

            return query.Count();
        }

        public long Size()
        {
            FileInfo f = new FileInfo(FullPath);
            long size = -1;
            if (f.Exists)
            {
                size = f.Length;
            } 
           
            f = null;

            return size;
        }

        /// <summary>
        /// Get RowCount of items in dataset
        /// </summary>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <returns>Row total</returns>
        public int RowCount<T>()
        {
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m); 
            return query.Count();
        }

        /// <summary>
        /// Retrieve full list of items in data set
        /// </summary>
        /// <remarks>
        /// Remember that a big XML file may crash your app
        /// </remarks>
        /// <typeparam name="T">IDiscioTable Object</typeparam>
        /// <returns>Full list of items</returns>
        public List<T> Full<T>()
        {
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>());

            return query.ToList<T>();
        }


        /// <summary>
        /// Select items in a datase with a transformative delegate "Func<T, TResult>" (select)
        /// </summary>
        /// <typeparam name="T">IDiscio type</typeparam>
        /// <typeparam name="TResult">Result type</typeparam>
        /// <param name="select">transformative delegate</param>
        /// <returns>Result</returns>
        public List<TResult> Select<T, TResult>(Func<T,TResult> select)
        {
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>()).Select(select).Take(DiscioSource.MaxRecords);

            return query.ToList<TResult>();
        }

        /// <summary>
        /// Select items in a datase with a transformative delegate "Func<T, TResult>" (select) and a comparative delegate "Func<T,bool>" (where)
        /// </summary>
        /// <typeparam name="T">IDiscio type</typeparam>
        /// <typeparam name="TResult">Result type</typeparam>
        /// <param name="select">transformative delegate</param>
        /// <param name="where">comparative delegate</param>
        /// <returns>Result</returns>
        public List<TResult> Select<T, TResult>(Func<T, TResult> select, Func<T, bool> where)
        {
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>())
                         .Where(where)
                         .Take(DiscioSource.MaxRecords)
                         .Select(select);

            return query.ToList<TResult>();
        }

        /// <summary>
        /// Select items in data set
        /// </summary>
        /// <remarks>
        /// Max items returned is 250.
        /// </remarks>
        /// <typeparam name="T">IDiscio type</typeparam>
        /// <returns>List of items</returns>
        public List<T> Select<T>()
        {
            XElement elem = XElement.Load(FullPath); 
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>()).Take(DiscioSource.MaxRecords);

            return query.ToList<T>();
        }

        /// <summary>
        /// Select items in data set with comparative delegate "Func<T,bool>" (where)
        /// </summary>
        /// <remarks>
        /// Max items returned is 250.
        /// </remarks>
        /// <typeparam name="T">IDiscio type</typeparam>
        /// <param name="where">where clause</param>
        /// <returns>List of items</returns>
        public List<T> Select<T>(Func<T, bool> where)
        {
            XElement elem = XElement.Load(FullPath);
            var query = (from m in elem.Elements(typeof(T).Name)
                         select m.ToXmlElement().DeserializeFromXmlElement<T>()).Where(where).Take(DiscioSource.MaxRecords);

            return query.ToList<T>();
        }

        /// <summary>
        /// Load single item by ID
        /// </summary>
        /// <typeparam name="T">IDiscio type</typeparam>
        /// <param name="ID">ID</param>
        /// <returns>IDiscuiTable item</returns>
        public T Load<T>(string ID)
        {
            T o = default(T);
            XElement elem = XElement.Load(FullPath);

            o = (from m in elem.Elements(typeof(T).Name)
                 where
                 (m.Element("ID").Value == ID)
                 select m.ToXmlElement().DeserializeFromXmlElement<T>()).FirstOrDefault();
            
            

            return o;
        }

        #endregion

       
        private void CreateGUIDs()
        { 
            for (int i = 0; i < ToInsert.Count; i++)
            {
                ToInsert[i].ID = Guid.NewGuid().ToString();
            }
        }
         
        /// <summary>
        /// Add item to be updated
        /// </summary>
        /// <param name="item">item</param>
        public void Update(IDiscio item)
        {
            //If an item was encrypted on insert don't do it again.
             ToUpdate.Add(item.EncryptHelper()); 
        }

        /// <summary>
        /// Add list of items to be updated
        /// </summary>
        /// <param name="items">Items list</param>
        public void Update(List<IDiscio> items)
        {
            foreach (var i in items)
            {
                Update(i);
            }
       
        }

        /// <summary>
        /// Add item to be deleted
        /// </summary>
        /// <param name="item">item</param>
        public void Delete(IDiscio item)
        {
            ToDelete.Add(item);
        }

        /// <summary>
        /// Add list of items to be ideleted
        /// </summary>
        /// <param name="items">Items list</param>
        public void Delete(List<IDiscio> items)
        {
            ToDelete.AddRange(items);
        }

        /// <summary>
        /// Add item to be inserted
        /// </summary>
        /// <param name="item">item</param>
        public void Insert(IDiscio item)
        { 
            ToInsert.Add(item.EncryptHelper());
        }

       

        /// <summary>
        /// Add list of items to be inserted
        /// </summary>
        /// <param name="items">Items list</param>
        public void Insert(List<IDiscio> items)
        {
            ToInsert.AddRange(items.EncryptHelper());
        }

        private string FullPath { set; get; }

        /// <summary>
        /// Location under the datafolder
        /// </summary>
        public string Location { set; get; }

        /// <summary>
        /// DataSet name
        /// </summary>
        public string SetName { set; get; }
        
        /// <summary>
        /// DataFolder name
        /// </summary>
        public string DataFolder { set; get; }
        
        /// <summary>
        /// DataSource status
        /// </summary>
        /// <remarks>
        /// It will be false if the manager is unable to access the DataFolder
        /// </remarks>
        public bool ValidSource { set; get; }

        private List<IDiscio> ToInsert
        {
            set;
            get;
        }

        private List<IDiscio> ToUpdate
        {
            set;
            get;
        }

        private List<IDiscio> ToDelete
        {
            set;
            get;
        }

        /// <summary>
        /// Encrypt a string using the local AES encryption class
        /// </summary>
        /// <param name="text">Text</param>
        /// <returns>Encrypted text</returns>
        public static string Encrypt(string text)
        {
            AES enc = new AES();
            return enc.EncryptToString(text);
        }

  


    }
}
